package sf.r2dbc.sql;

import io.r2dbc.spi.Blob;
import io.r2dbc.spi.Clob;
import io.r2dbc.spi.Statement;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.core.convert.support.GenericConversionService;
import org.springframework.data.convert.Jsr310Converters;
import reactor.core.publisher.Mono;
import sf.database.jdbc.type.AttributeConverterType;
import sf.database.jdbc.type.TypeHandler;
import sf.database.meta.ColumnMapping;
import sf.r2dbc.binding.BindMarker;

import javax.persistence.AttributeConverter;
import javax.persistence.EnumType;
import javax.persistence.TemporalType;
import java.io.ByteArrayOutputStream;
import java.io.StringWriter;
import java.nio.ByteBuffer;
import java.sql.Time;
import java.sql.Timestamp;
import java.time.Duration;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.OffsetDateTime;
import java.time.OffsetTime;
import java.util.Date;
import java.util.concurrent.CountDownLatch;

public class R2dbcConvertUtils {
    private static Logger log = LoggerFactory.getLogger(R2dbcConvertUtils.class);

    static GenericConversionService CONVERSION_SERVICE = new DefaultConversionService();

    static {
        Jsr310Converters.getConvertersToRegister().forEach(CONVERSION_SERVICE::addConverter);
    }

    public static <T, N> T get(N dbData, Class<N> dbDataClass, ColumnMapping cm) {
        AttributeConverter<?, ?> attributeConverter = null;
        TypeHandler<?> typeHandler = null;
        Class<?> clz = null;
        if (cm != null) {
            clz = cm.getClz();
            typeHandler = cm.getHandler();
            if (typeHandler.getClass() == AttributeConverterType.class) {
                attributeConverter = ((AttributeConverterType) typeHandler).getAttributeConverter();
            }
        }
        return (T) convertToEntityAttribute(dbData, dbDataClass, clz, (AttributeConverter) attributeConverter);
    }

    public static <T, N> T get(N dbData, Class<N> dbDataClass, Class<T> targetClass, AttributeConverter<?, ?> attributeConverter, TypeHandler<?> typeHandler) {
        return (T) convertToEntityAttribute(dbData, dbDataClass, targetClass, (AttributeConverter) attributeConverter);
    }

    public static void set(BindMarker spec, Statement target, Object value,
                           Class<?> targetClass, AttributeConverter<?, ?> attributeConverter, TemporalType temporalType, EnumType enumType) {
        if (targetClass == null) {
            targetClass = Object.class;
        }
        if (value == null) {
            spec.bindNull(target, fixBugs(targetClass, temporalType));
        } else {
            spec.bind(target, convertToDatabaseColumn(value, targetClass, (AttributeConverter) attributeConverter, temporalType, enumType));
        }
    }

    public static Statement set(Statement spec, Object value, int index,
                                Class<?> targetClass, AttributeConverter<?, ?> attributeConverter, TemporalType temporalType, EnumType enumType) {
        if (targetClass == null) {
            targetClass = Object.class;
        }
        if (value == null) {
            return spec.bindNull(index, targetClass);
        } else {
            return spec.bind(index, convertToDatabaseColumn(value, targetClass, (AttributeConverter) attributeConverter, temporalType, enumType));
        }
    }

    public static Statement set(Statement spec, Object value, String parameterName,
                                Class<?> targetClass, AttributeConverter<?, ?> attributeConverter, TemporalType temporalType, EnumType enumType) {
        if (targetClass == null) {
            targetClass = Object.class;
        }
        if (value == null) {
            return spec.bindNull(parameterName, targetClass);
        } else {
            return spec.bind(parameterName, convertToDatabaseColumn(value, targetClass, (AttributeConverter) attributeConverter, temporalType, enumType));
        }
    }

    /**
     * 支持jpa的原生转换注解
     * @param attribute
     * @param targetClass
     * @param converter
     * @param temporalType
     * @param enumType
     * @param <X>
     * @param <Y>
     * @return
     * @see javax.persistence.Convert
     * @see AttributeConverter
     */
    private static <X, Y> Y convertToDatabaseColumn(X attribute, Class<Y> targetClass, AttributeConverter<X, Y> converter, TemporalType temporalType, EnumType enumType) {
        if (attribute == null) {
            return null;
        }
        if (targetClass == null) {
            targetClass = (Class<Y>) Object.class;
        }
        if (converter != null) {
            return converter.convertToDatabaseColumn(attribute);
        }

        return (Y) fixBugs(attribute, targetClass, temporalType, enumType);
    }

    /**
     * 支持jpa的原生转换注解
     * @param dbData
     * @param targetClass
     * @param converter
     * @param <X>
     * @param <Y>
     * @return
     * @see javax.persistence.Convert
     * @see AttributeConverter
     */
    private static <X, Y> X convertToEntityAttribute(Y dbData, Class<Y> dbDataClass, Class<X> targetClass, AttributeConverter<X, Y> converter) {
        if (dbData == null || (targetClass != null && targetClass == dbData.getClass())) {
            return (X) dbData;
        }
        if (converter != null) {
            return converter.convertToEntityAttribute(dbData);
        }
        if (targetClass == null) {
            targetClass = (Class<X>) Object.class;
        }
        if (dbDataClass == null) {
            dbDataClass = (Class<Y>) dbData.getClass();
        }
        Object data = dbData;
        //以下是特殊转换
        if (ByteBuffer.class.isAssignableFrom(dbDataClass)) {
//            ByteBuffer buffer = (ByteBuffer) dbData;
//            byte[] bytes = new byte[buffer.remaining()];
//            for (int i = 0; i < bytes.length; i++) {
//                bytes[i] = buffer.get(i);
//            }
//            data = bytes;
        } else if (Blob.class.isAssignableFrom(dbDataClass)) {
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            Blob blob = (Blob) dbData;
            CountDownLatch latch = new CountDownLatch(1);
            Mono.from(blob.stream()).doFinally(signalType -> latch.countDown()).subscribe(buffer -> {
                while (buffer.hasRemaining()) {
                    baos.write(buffer.get());
                }
                buffer.clear();
            });
            try {
                latch.await();
            } catch (InterruptedException e) {
                log.error("", e);
            }
            data = baos.toByteArray();
        } else if (Clob.class.isAssignableFrom(dbDataClass)) {
            StringWriter sw = new StringWriter();
            CountDownLatch latch = new CountDownLatch(1);
            Clob clob = (Clob) dbData;
            Mono.from(clob.stream()).doFinally(signalType -> latch.countDown()).subscribe(sw::append);
            try {
                latch.await();
            } catch (InterruptedException e) {
                log.error("", e);
            }
            data = sw.toString();
        } else if (dbDataClass == Duration.class) {
            data = new Date(((Duration) dbData).toMillis());
        } else if (dbDataClass == LocalTime.class) {
            data = Time.valueOf((LocalTime) dbData);
        } else if (dbDataClass == LocalDate.class) {
            data = java.sql.Date.valueOf((LocalDate) dbData);
        } else if (dbDataClass == LocalDateTime.class) {
            data = Timestamp.valueOf((LocalDateTime) dbData);
        } else if (dbDataClass == OffsetTime.class) {
            data = Time.valueOf(((OffsetTime) dbData).toLocalTime());
        } else if (dbDataClass == OffsetDateTime.class) {
            data = Timestamp.valueOf(((OffsetDateTime) dbData).toLocalDateTime());
        }
//        if (ByteBuffer.class.isAssignableFrom(dbDataClass) && targetClass == Object.class) {
//            targetClass = (Class<X>) byte[].class;
//        }
//        if (CONVERSION_SERVICE.canConvert(data.getClass(),targetClass)){
//            return CONVERSION_SERVICE.convert(data, targetClass);
//        }
        return CONVERSION_SERVICE.convert(data, targetClass);
    }

    /**
     * 1.r2dbc时间只认jdk8的时间<br>
     * 2.枚举的处理.
     * @param val
     * @param targetClass
     * @param temporalType
     * @param enumType
     * @return
     */
    public static Object fixBugs(Object val, Class<?> targetClass, TemporalType temporalType, EnumType enumType) {
        if (Date.class.isAssignableFrom(val.getClass())) {
            Date date = (Date) val;
            if (temporalType != null) {
                switch (temporalType) {
                    case TIME:
                        return new Time(date.getTime()).toLocalTime();
                    case DATE:
                        return new java.sql.Date(date.getTime()).toLocalDate();
                    case TIMESTAMP:
                        return new Timestamp(date.getTime()).toLocalDateTime();
                    default:
                        break;
                }
            }
            //默认为时间戳
            return new Timestamp(date.getTime()).toLocalDateTime();
        } else if (val.getClass().isEnum()) {
            if (enumType == EnumType.ORDINAL) {
                return ((Enum<?>) val).ordinal();
            }
            return val.toString();
        }
        return CONVERSION_SERVICE.convert(val, targetClass);
    }

    public static Class<?> fixBugs(Class<?> valueClass, TemporalType temporalType) {
        if (Date.class.isAssignableFrom(valueClass)) {
            if (temporalType != null) {
                switch (temporalType) {
                    case TIME:
                        return LocalTime.class;
                    case DATE:
                        return LocalDate.class;
                    case TIMESTAMP:
                        return LocalDateTime.class;
                    default:
                        break;
                }
            }
            //默认为时间戳
            return LocalDateTime.class;
        }
        return valueClass;
    }
}
